import threading
import ftputil
import ftputil
import os
import os.path
import threading
import stat
import re
import shutil
import time
import possum

# *******************************************************************
# Thread for scanning a directory tree and compiling a list of files
# with size and modification times
#
# sourcefs and destfs should be objects supporting the following
# methods:
#  * listdir
#  * isdir
#  * stat
#  * join
#
# dirstoignore should be a list of regular expressions. Any directory
# name that matches any of these expressions will not be counted in
# the list
class ScanThread(threading.Thread):
	
	# -------------------------------------------------------------------
	def __init__(self, sourcefs, destfs, sourcedir, destdir, dirstoignore, log):
		threading.Thread.__init__(self)
		
		self.sourcefs = sourcefs
		self.sourcedir = sourcedir
		self.destfs = destfs
		self.destdir = destdir
		self.dirstoignore = dirstoignore
		
		self.dirs = {}
		self.files = {}
		self.cancel = False
		self.log = log
		
	# -------------------------------------------------------------------
	# Ends the currently running thread at the next opportunity.
	# This method may take a couple of seconds to complete depending on
	# the speed of the underlying file system object
	def Cancel(self):
		self.cancel = True
		
	# -------------------------------------------------------------------
	def ScanDir(self, sourcedir, destdir):
		self.dirs[sourcedir] = destdir		
		
		if self.cancel:
			return
		
		self.log.write('Scanning ' + sourcedir)
		
		ls = []
		
		try:
			ls = self.sourcefs.listdir(sourcedir)
		except EnvironmentError, e:
			print e
		
		for f in ls:
			if self.cancel:
				return			
			
			if sourcedir == '/':
				fullsource = '/' + f
			else:
				fullsource = sourcedir + '/' + f
				
			if destdir == '/':
				fulldest = '/' + f
			else:
				fulldest = destdir + '/' + f
			
			if self.sourcefs.isdir(fullsource):
				ignore = False
				
				for r in self.dirstoignore:
					if r.match(f):
						ignore = True
						break
				
				if not ignore:
					self.ScanDir(fullsource, fulldest)
			else:
				stats = self.sourcefs.stat(fullsource)
				self.files[fullsource] = (self.destdir 
																+ fullsource[ len(self.sourcedir) : ], 
															stats[stat.ST_SIZE], 
															stats[stat.ST_MTIME])
		
	# -------------------------------------------------------------------
	def run(self):
		self.log.write('Starting scans for "%s" to "%s"' % (self.sourcedir, self.destdir) )
		self.ScanDir( self.sourcedir, self.destdir )
		self.log.write('Finishing scanning for "%s" to "%s"' % (self.sourcedir, self.destdir) )

# *********************************************************************
class FileSync:
	(	COPY,
		SYNC,
		DELETE,
	) = range(0x00, 0x03)	
	
	(	WAITING,
		SCANNING,
		SCANNING_FINISHED,
		CALCULATING,
		CALCULATING_FINISHED,
		SYNCING,
		SYNCING_FINISHED,
		CANCELED,
	) = range(0x00, 0x08)	
	
	# *******************************************************************
	class filesys:
		def __init__(self):
			self.listdir = os.listdir
			self.isdir = os.path.isdir
			self.copy = shutil.copy2
			self.stat = os.stat
			self.makedirs = os.makedirs
			self.rmdir = os.rmdir
			self.remove = os.remove
			self.join = os.path.join
	
	# -------------------------------------------------------------------
	def __init__(self, log = possum.StdoutLog):
		self.sourcefs = self.filesys()
		self.destfs = self.filesys()
		
		self.sourcefiles = {}
		self.destfiles = {}
		self.sourcedirs = {}
		self.destdirs = {}
		self.destdirstodelete = {}
		self.destfilestodelete = {}
		
		self.sourcethread = None
		self.destthread = None
		
		self.log = log
		self.mode = self.COPY
		self.cancel = False
		
		self.status = self.WAITING
	
	# -------------------------------------------------------------------
	def Cancel(self):
		if self.sourcethread:
			if self.sourcethread.isAlive():
				self.sourcethread.Cancel()
				
		if self.destthread:
			if self.destthread.isAlive():
				self.destthread.Cancel()
		
		self.cancel = True
		self.status = self.CANCELED
		
	# -------------------------------------------------------------------
	def Scan(self, sourcedir, destdir, dirstoignore = []):
		self.status = self.SCANNING
		dirres = []
		
		for d in dirstoignore:
			dirres.append( re.compile(d) )
		
		self.sourcethread = ScanThread(self.sourcefs, self.destfs, sourcedir, destdir, dirres, self.log)
		self.destthread = ScanThread(self.destfs, self.sourcefs, destdir, sourcedir, dirres, self.log)
		
		self.sourcethread.start()
		self.destthread.start()
		
	# -------------------------------------------------------------------
	def CalculateUpdates(self):
		if self.cancel:
			return
		
		self.status = self.CALCULATING
		
		# Create directories
		self.log.write('Calculating updates')
		for k, v in self.sourcethread.dirs.iteritems():
			if not self.destthread.dirs.has_key(v):
				self.sourcedirs[k] = v		
		
		for k, v in self.sourcethread.files.iteritems():
			# Source file does not exist at destination
			if not self.destthread.files.has_key(v[0]):
				self.sourcefiles[k] = v
				self.log.write('Adding "%s" >>> "%s": missing file' % (k, v[0]) )
			else:
				destv = self.destthread.files[v[0]]
				
				# Same size file
				if destv[1] == v[1]:
					# Copy if source is newer
					
					if v[2] > destv[2]:
						self.sourcefiles[k] = v
						td = v[2] - destv[2]
						self.log.write('Adding "%s" >>> "%s": newer by %d-%02d:%02d:%02d' % (k, v[0], td / 86400, (td % 86400) / 3600, ( (td % 86400) % 3600) / 60, td % 60) )
					
					continue
				
				# Different size files, operation depends on' mode
				if self.mode == self.COPY:
					# Copy uses automatic overwrite					
					self.sourcefiles[k] = v
					self.log.write('Adding "%s" >>> "%s": size difference' % (k, v[0]) )
				elif self.mode == self.SYNC or self.mode == self.DELETE:
					# Sync and delete modes overwrite on newer times
					if v[2] > destv[2]:
						self.sourcefiles[k] = v
						td = v[2] - destv[2]
						self.log.write('Adding "%s" >>> "%s": newer by %d-%02d:%02d:%02d' % (k, v[0], td / 86400, (td % 86400) / 3600, ( (td % 86400) % 3600) / 60, td % 60) )
					# If destination time is newer
					elif destv[2] > v[2]:
						td = destv[2] - v[2]
						self.destfiles[v[0]] = (k, destv[1], destv[2])
						self.log.write('Adding "%s" <<< "%s": older by %d-%02d:%02d:%02d' % (k, v[0], td / 86400, (td % 86400) / 3600, ( (td % 86400) % 3600) / 60, td % 60) )
		
		if self.mode == self.SYNC:
			for k, v in self.destthread.dirs.iteritems():
				if not self.sourcethread.dirs.has_key(v):
					self.destdirs[k] = v	
		elif self.mode == self.DELETE:			
			for k, v in self.destthread.dirs.iteritems():
				if not self.sourcethread.dirs.has_key(v):
					self.destdirstodelete[k] = v	
			
		for k, v in self.destthread.files.iteritems():
			# Destination file does not exist at destination
			if not self.sourcethread.files.has_key(v[0]):
				if self.mode == self.SYNC:
					self.destfiles[k] = v
					self.log.write('Adding "%s" <<< "%s": missing file' % (v[0], k) )
				elif self.mode == self.DELETE:
					self.destfilestodelete[k] = v
					self.log.write('Adding "%s" to delete' % (k, ) )
					
		numfiles = len(self.sourcefiles) + len(self.destfiles)
		numbytes = 0
		
		for v in self.sourcefiles.itervalues():
			numbytes += v[1]
		
		for v in self.destfiles.itervalues():
			numbytes += v[1]
			
		self.log.write('Finished calculating: %d files to copy totalling %.2f KiB' 
									% (numfiles, numbytes / 1024.0) )
									
		self.status = self.CALCULATING_FINISHED
						
	# -------------------------------------------------------------------
	def Sync(self):
		if self.cancel:
			return
		
		self.status = self.SYNCING
		# Files and bytes are recalculated here in case multiple
		# scans have been conducted
		self.numfiles = len(self.sourcefiles) + len(self.destfiles)
		self.numbytes = 0
		self.filescopied = 0
		self.bytescopied = 0
		
		for v in self.sourcefiles.itervalues():
			self.numbytes += v[1]
		
		for v in self.destfiles.itervalues():
			self.numbytes += v[1]
		
		for k, v in self.sourcedirs.iteritems():
			if self.cancel:
				return
			try:
				self.destfs.makedirs(v)
			except OSError, e:
				if e.errno == 17:
					continue
				
				self.log.write('Error creating "%s": %s' % (v, e) )
		
		if self.mode == self.SYNC:
			for k, v in self.destdirs.iteritems():
				if self.cancel:
					return
				try:
					self.sourcefs.makedirs(v)
				except OSError, e:
					if e.errno == 17:
						continue
					
					self.log.write('Error creating "%s": %s' % (v, e) )
		
		starttime = time.time()
		
		for k, v in self.sourcefiles.iteritems():
			if self.cancel:
				return
			try:
				self.log.write('Copying %d/%d, %.2f/%.2f KiB: %s >>> %s (%.2f KiB)'
												% (self.filescopied + 1, self.numfiles, self.bytescopied / 1024.0, self.numbytes / 1024.0, k, v[0], v[1] / 1024.0 ) )
				self.sourcefs.copy(k, v[0])
				self.filescopied += 1
				self.bytescopied += v[1]
			except EnvironmentError, e:
				self.log.write('Error copying "%s" >>> "%s": %s' % (k, v[0], e) )
		
		endtime = time.time()
		
		if self.mode == self.SYNC or self.mode == self.DELETE:
			for k, v in self.destfiles.iteritems():
				if self.cancel:
					return
				self.log.write('Copying %d/%d, %.2f/%.2f KiB: %s <<< %s (%.2f KiB)'
												% (self.filescopied + 1, self.numfiles, self.bytescopied / 1024.0, self.numbytes / 1024.0, k, v[0], v[1] / 1024.0 ) )
				try:
					self.destfs.copy(k, v[0])				
					self.filescopied += 1
					self.bytescopied += v[1]
				except EnvironmentError, e:
					self.log.write('Error copying "%s" <<< "%s": %s' % (k, v[0], e) )
					
			endtime = time.time()
			
		if self.mode == self.DELETE:
			for k, v in self.destfilestodelete.iteritems():
				if self.cancel:
					return
				try:
					self.log.write('Deleting "%s"' % (k, ) )
					self.destfs.remove(k)			
				except EnvironmentError, e:
					self.log.write('Error deleting "%s": %s' % (k,  e) )
		
		if self.mode == self.DELETE:
			for k, v in self.destdirstodelete.iteritems():
				if self.cancel:
					return
				try:
					self.destfs.rmdir(k)
				except EnvironmentError, e:
					self.log.write('Error deleting "%s": %s' % (k, e) )
		
		self.log.write('Finished copying: %d files totalling %.2f KiB copied in %.2f seconds: %.2f KiB/s'
			% (self.filescopied, self.bytescopied / 1024.0, self.endtime - self.starttime, self.bytescopied / 1024.0 / (self.endtime - self.starttime) ) )
		self.status = self.SYNCING_FINISHED

# *********************************************************************
class FTPSync(FileSync):
	
	# -------------------------------------------------------------------
	def __init__(self, host, username, password):
		FileSync.__init__(self)
		self.ftp = ftputil.FTPHost(host, username, password)
		self.ftp.synchronize_times()
		self.timeshift = self.ftp.time_shift()
		ts = self.timeshift
	
	# -------------------------------------------------------------------
	def SetDownload(self, d = True):	
		self.sourcefs = self.filesys()
		self.destfs = self.filesys()
		
		if d:
			self.sourcefs.listdir = self.ftp.listdir
			self.sourcefs.isdir = self.ftp.path.isdir
			self.sourcefs.copy = self.Download
			self.destfs.copy = self.Upload
			self.sourcefs.stat = self.ftp.stat
			self.sourcefs.makedirs = self.ftp.makedirs
			self.sourcefs.rmdir = self.ftp.rmdir
			self.sourcefs.remove = self.ftp.remove
		else:
			self.destfs.listdir = self.ftp.listdir
			self.destfs.isdir = self.ftp.path.isdir
			self.sourcefs.copy = self.Upload
			self.destfs.copy = self.Download
			self.destfs.stat = self.ftp.stat
			self.destfs.makedirs = self.ftp.makedirs
			self.destfs.rmdir = self.ftp.rmdir
			self.destfs.remove = self.ftp.remove
			
	# -------------------------------------------------------------------
	def Download(self, source, dest):	
		self.ftp.download(source, dest, 'b')
		s = self.ftp.stat(source)
		mtime = s[stat.ST_MTIME]
		os.utime(dest, (mtime, mtime) )
		
	# -------------------------------------------------------------------
	def Upload(self, source, dest):	
		self.ftp.upload(source, dest, 'b')
		s = self.ftp.stat(dest)
		mtime = s[stat.ST_MTIME]
		os.utime(source, (mtime, mtime) )
